import argparse  # 标准库,解析命令行参数和选项
import tkinter as tk  # 标准库, 提供图形用户界面（GUI）工具包
from tkinter import messagebox  # tkinter 的子模块, 提供简单的消息框对话框
from datetime import datetime
from pathlib import Path  # 标准库, 提供面向对象的路径操作，简化文件路径处理
import os  # 标准库, 提供与操作系统交互的功能，如文件和目录操作
import sys  # 标准库, 提供对解释器使用或维护的一些变量的访问以及与解释器进行交互的函数
from typing import Optional, List  # 标准库, 提供类型提示支持，增强代码可读性和维护性
import re
import readconfig  # 自定义模块, 用于读取配置文件，通常包含配置解析和管理功能
import yzd_lib_RightClickMenu
from yzd_lib_RightClickMenu import install_menu, uninstall_menu, 右键菜单,showitems,setmaxfile

from pydantic import BaseModel  # 第三方库, 提供数据验证和数据类功能，常用于快速构建数据模型

'''
本程序最终将打包成exe文件，运行时默认会有黑色控制台窗口，不方便使用。因此打包命令中设置了控制台不可见。
  这样具体调用时候不会有讨厌的黑窗口，但是副作用就是也没法提示用户安装菜单完毕或卸载菜单完毕。
  因此使用tkinter显示消息框提示用户相关结果。
'''
def show_message(title, message):
    root = tk.Tk()
    root.withdraw()  # 隐藏主窗口
    messagebox.showinfo(title, message)
    root.destroy()

# 定义全局变量
默认菜单定义配置文件名 = None
默认屏蔽字符串列表配置文件名 = None
def 初始化配置文件名():
    global 默认菜单定义配置文件名, 默认屏蔽字符串列表配置文件名
    # 获取当前脚本的绝对路径和文件名（不含后缀）
    # 当前脚本路径 = os.path.abspath(__file__) #不能用这个如果用pyinstaller打包，这里获取的是运行时解包的临时目录，例如 C:\Users\yzd\AppData\Local\Temp\_MEI76042\
    当前脚本路径 = str(Path(sys.argv[0]).resolve()) #只能用这个，无论是否打包，都能正确运行
    当前脚本目录 = os.path.dirname(当前脚本路径)
    当前脚本文件名 = os.path.splitext(os.path.basename(当前脚本路径))[0]
    # 推算配置文件名（裸文件名）
    菜单定义配置文件名 = f"{当前脚本文件名}_配置.ini"
    屏蔽字符串列表配置文件名 = f"{当前脚本文件名}_过滤.ini"
    # 推算全路径文件名
    def 推算全路径(文件名):
        # 检查当前脚本所在盘的根目录下是否有该文件
        根目录 = os.path.splitdrive(当前脚本路径)[0] + os.sep
        根目录文件路径 = os.path.join(根目录, 文件名)
        print("查找： ",根目录文件路径,end=" ")
        if os.path.exists(根目录文件路径):
            print("成功.")
            return 根目录文件路径
        print("失败.")
        # 检查当前脚本所在目录下是否有该文件
        当前目录文件路径 = os.path.join(当前脚本目录, 文件名)
        print("查找： ", 当前目录文件路径,end=" ")
        if os.path.exists(当前目录文件路径):
            print("成功.")
            return 当前目录文件路径
        print("失败.")
        # 如果两个地方都没有找到，则返回 None
        return None
    # 分别推算两个配置文件的全路径
    默认菜单定义配置文件名 = 推算全路径(菜单定义配置文件名)
    默认屏蔽字符串列表配置文件名 = 推算全路径(屏蔽字符串列表配置文件名)

def 安装右键菜单():
    # 加载配置文件，从配置文件获取菜单定义list
    if 默认菜单定义配置文件名==None:
        show_message("👣提示👣", "配置文件没找到")
        sys.exit(0)
    菜单定义list = readconfig.解析菜单配置文件(默认菜单定义配置文件名)
    # 依据配置文件 生成 注册表修改任务
    print("菜单注册表修改任务list")
    # 修改任务中需要设置菜单命令行，命令行要用到本程序全路径,
    # 为了兼容打包后exe运行环境，这里不能用__file__，只能用argv
    本程序全路径文件名 = str(Path(sys.argv[0]).resolve())
    菜单注册表修改任务list = []
    if len(菜单注册表修改任务list)>20:
        show_message("👣提示👣", "为避免影响效率，本程序最多添加20个根菜单")
        return
    for i, rec in enumerate(菜单定义list, 1):
        print(f"----------------菜单注册表修改任务 {i}---------------------")
        temp = 菜单定义对象to注册表修改对象(rec, i - 1, 本程序全路径文件名)
        temp.菜单名称 = f"mymenuv2_{i:03d}"
        菜单注册表修改任务list.append(temp)
        _debug_ShowMenuStru(temp)
    for i,m in enumerate(菜单注册表修改任务list):
        print(f"======================= 创建菜单组{i+1}============================")
        install_menu(m, path=r"SOFTWARE\Classes\*\shell")
        install_menu(m, path=r"SOFTWARE\Classes\Directory\shell")
        yzd_lib_RightClickMenu.setmaxfile(99)

def 移除右键菜单():
    for i in range(20):#默认最多安装20个
        print(
            f"======================= 清除菜单组{i}============================")  # temp.菜单名称 = f"mymenuv2_{i:03d}"
        uninstall_menu([f"mymenuv2_{i:03d}"], path=r"SOFTWARE\Classes\*\shell")  # 移除文件右键菜单
        uninstall_menu([f"mymenuv2_{i:03d}"], path=r"SOFTWARE\Classes\Directory\shell")  # 移除文件夹右键菜单
        yzd_lib_RightClickMenu.setmaxfile()
    print("右键菜单已成功移除。")

def 文件名打标签(fn: str, groupindex: int, s: str) -> str:
    # 加载配置文件，从配置文件获取菜单定义list
    菜单定义list = readconfig.解析菜单配置文件(默认菜单定义配置文件名)
    # 依据配置文件，获取前导字符分组
    前导符分组List = []
    for i, rec in enumerate(菜单定义list, 1):
        temp = 收集前导符(rec)
        前导符分组List.append(temp)
        print(f"{i}: {temp}")
    # 获取需要屏蔽的字符串列表
    init_ignore_words(默认屏蔽字符串列表配置文件名)

    # 传入的是目标路径，用Path规范化路径
    old_path_name= Path(fn).resolve()
    # 如果目标不存在，直接退出
    if not old_path_name.exists():
        print(f"错误: 未找到要改名的文件或目录 {fn}")
        sys.exit(1)
    # 获取原始文件名
    old_fn = old_path_name.name  #获取裸文件名
    old_fn = ignore_word_in_fn(old_fn) #剔除过滤字符串
    # 如果原来有时间戳，删除之
    old_fn=剔除文件名yyyymmdd前缀(old_fn)
    # 首先进行文件名还原，获取不含标签的裸文件名，标签原始状态记录在r中
    r, final_fn = 文件名中删除标签(old_fn, 前导符分组List)
    # 根据本次打标指令字符串s，调整标签记录r内容
    if s == "":# 如果s=="",也就是该组不用打标，只需要列表r中的groupindex位置设-1
        r[groupindex] = -1  # 清除工作
    else:
        if s in 前导符分组List[groupindex]:
            r[groupindex] = 前导符分组List[groupindex].index(s)
        else:
            raise ValueError(f"字符串 '{s}' 不在模式列表的第 {groupindex} 个子列表中")
    # 现在r是调整后的打标状态，使用更新后的r对文件名执行文件名打标
    new_fn = 文件名中添加标签(final_fn, 前导符分组List, r)
    # 构建新全路径文件名
    new_path_name = old_path_name.with_name(new_fn)
    # 执行改名
    try:
        os.rename(old_path_name, new_path_name)
        print(f"成功将 {old_path_name} 改名为 {new_path_name}")
    except Exception as e:
        print(f"错误: 无法改名 {old_path_name} 为 {new_path_name}: {e}")
        sys.exit(1)
def 文件名设置时间戳记(fn, mode):
    """
    根据传入的 mode 决定使用文件的创建时间或最后修改时间来生成新的文件名，
    并在文件名最前面添加 yyyymmdd 格式的日期字符串。
    参数:
    file_path (str): 文件的全路径。
    mode (int): 0 表示使用创建时间，1 表示使用最后修改时间。
    返回:
    str: 新文件的全路径，如果传入的是目录则返回 None。
    """
    # 检查是否为文件,添加时间戳只对文件有意义
    if not Path(fn).is_file():
        print(f"传入的路径 '{fn}' 不是文件，不进行任何操作。")
        return None
    # 获取需要屏蔽的字符串列表
    init_ignore_words(默认屏蔽字符串列表配置文件名)
    # 传入的是目标路径，用Path规范化路径
    old_path_name= Path(fn).resolve()
    # 获取原始文件名
    old_fn = old_path_name.name  #获取裸文件名
    old_fn = ignore_word_in_fn(old_fn) #剔除过滤字符串
    # 如果原来有时间戳，删除之
    Unprefixed_oldfn=剔除文件名yyyymmdd前缀(old_fn)

    # 根据 mode 决定选择使用时间戳
    if mode == "修改时间":
        print("获取最后修改时间：",end="")
        timestamp = old_path_name.stat().st_mtime
    elif mode == "创建时间":
        print("获取文件创建时间：", end="")
        timestamp = old_path_name.stat().st_ctime
    else:
        print(f"无效的 mode 值: {mode}。请使用 ‘修改时间’ 或 ‘创建时间’。")
        return None
    date_str = datetime.fromtimestamp(timestamp).strftime('%Y%m%d')# 将时间戳转换为 yyyymmdd 格式的字符串
    print(date_str)
    #时间戳添加
    new_fn=f"{date_str}_{Unprefixed_oldfn}"
    # 构建新全路径文件名
    new_path_name = old_path_name.with_name(new_fn)
    # 执行改名
    try:
        os.rename(old_path_name, new_path_name)
        print(f"成功将 {old_path_name} 改名为 {new_path_name}")
    except Exception as e:
        print(f"错误: 无法改名 {old_path_name} 为 {new_path_name}: {e}")
        sys.exit(1)
# --------------------------------------------------------
ignore_words: List[str]=[]
def init_ignore_words(fn):
    print(fn)
    global ignore_words
    try :
        with open(fn,"r",encoding="utf-8") as f:
            for line in f:
                temp=line.strip()
                if line:
                    ignore_words.append(temp)
    except FileNotFoundError:
        pass
def ignore_word_in_fn(fn:str)->str:
    def remove_key_with_spaces(nfn, key):
        # 第一步：删除 key 及其前后任意数量的空格
        pattern = r'\s*' + re.escape(key) + r'\s*'
        nfn_cleaned = re.sub(pattern, '', nfn)
        # 第二步：清理多余的空格，将多个空格替换为单个空格，并去除开头和结尾的空格
        nfn_cleaned = re.sub(r'\s+', ' ', nfn_cleaned).strip()
        return nfn_cleaned
    global ignore_words
    nfn=fn
    for key in ignore_words:
        nfn=remove_key_with_spaces(nfn,key)
    return nfn
def 文件名中删除标签(fn: str, mode: list[list[str]]) -> tuple[list[int], str]:
    # 去除fn的前后空白字符
    fn = fn.strip()

    r = [-1] * len(mode)  # 初始化r数组，长度为mode次级列表个数，值为-1

    while True:
        made_change = False  # 标记这一轮是否有变化
        for i, sub_list in enumerate(mode):
            for j, prefix in enumerate(sub_list):
                if fn.startswith(prefix):  # 如果fn以prefix开头
                    r[i] = j  # 记录子列表索引和匹配字符串索引
                    fn = fn[len(prefix):].strip()  # 移除fn前缀并再次去除前后空白字符
                    made_change = True  # 标记有变化
                    break  # 找到匹配后立即退出当前子列表的循环
            if made_change:  # 如果有变化就跳出外层循环，重新开始
                break

        if not made_change:  # 如果这一轮没有任何变化，结束循环
            break
    return (r, fn)  # 返回匹配结果数组r和最终的裸文件名
def 文件名中添加标签(fn: str, mode: list[list[str]], flag: list[int]) -> str:
    # 创建一个空的临时字符串用于存储所有前缀
    prefix = ""
    # 遍历flag数组，根据其值从mode中获取对应的前缀并添加到prefix
    for i, index in enumerate(flag):
        if index != -1:
            # 获取mode中第i个子列表的第index个元素（即前缀），并添加到prefix
            prefix += mode[i][index]
    # 将所有前缀添加到fn前面，并返回结果
    return prefix + fn

def 剔除文件名yyyymmdd前缀(file_name):
    """
    清除文件名中可能存在的 yyyymmdd 前缀。

    参数:
    file_name (str): 原始文件名。

    返回:
    str: 清除日期前缀后的文件名。
    """
    # 检查文件名是否以 yyyymmdd 格式的日期开头
    if len(file_name) >= 8 and file_name[:8].isdigit():
        try:
            # 尝试将前8个字符转换为日期
            datetime.strptime(file_name[:8], '%Y%m%d')
            # 如果成功，移除这8个字符及其后的下划线（如果有）
            if file_name[8] == '_':
                return file_name[9:]
            else:
                return file_name[8:]
        except ValueError:
            # 如果转换失败，说明文件名不包含有效的日期前缀，直接返回原文件名
            return file_name
    return file_name

def 菜单定义对象to注册表修改对象(root: readconfig.菜单项, 组号:int, 程序名:str) -> 右键菜单:
    newrec=右键菜单(
        菜单文字=root.菜单文字,
    )
    if root.下级菜单==None:
        if newrec.菜单文字=="⬜添加最后修改时间⬜":
            newrec.执行命令=f'{程序名} -timemark "%1" 修改时间"'
        elif newrec.菜单文字=="⬜添加文件创建时间⬜":
            newrec.执行命令=f'{程序名} -timemark "%1" 创建时间"'
        else:
            newrec.执行命令=f'{程序名} -r "%1" {组号} "{root.前导符}"'
    else:
        newrec.下级菜单=[]
        for subm in root.下级菜单:
            newrec.下级菜单.append(菜单定义对象to注册表修改对象(subm, 组号, 程序名))
    return newrec
def 收集前导符(menu_item: readconfig.菜单项) -> List[Optional[str]]:
    """
    递归遍历菜单项树，收集所有节点的前导符，并返回一个列表。
    :param menu_item: 根菜单项对象
    :return: 包含所有非 None 前导符的列表
    """
    # 初始化结果列表
    lead_chars = []
    # 如果当前节点有前导符且不是 None，则添加到结果列表中
    if menu_item.前导符 is not None and menu_item.前导符!="<清除>" and menu_item.前导符.strip()!="":
        lead_chars.append(menu_item.前导符)
    # 如果当前节点有下级菜单，则递归遍历每个子节点
    if menu_item.下级菜单:
        for child in menu_item.下级菜单:
            lead_chars.extend(收集前导符(child))
    return lead_chars

def to_integer(value):
    """
    将输入的字符串转换为整数，并返回整数。
    参数:    value (str): 需要转换的字符串。
    返回:    int: 转换后的整数。
    异常:    argparse.ArgumentTypeError: 如果转换失败，抛出此异常。
    """
    try:
        return int(value)
    except ValueError:
        raise argparse.ArgumentTypeError(f"MARK_GROUP是无效的整数值: {value}。请提供一个有效的整数。")
def _debug_ShowMenuStru(root: 右键菜单, tab=0):
    if root.下级菜单:
        print(f"{tab * '↔ '}【{root.菜单文字}】", root.执行命令,root.下级菜单)
        for r in root.下级菜单:
            _debug_ShowMenuStru(r, tab + 1)
    else:
        print(f"{tab * '↔ '}[{root.菜单文字}]", root.执行命令,root.下级菜单)

########################################################################
def main():
    # 获取命令行
    parser = argparse.ArgumentParser(description="右键菜单管理工具")
    parser.add_argument('-i', action='store_true', help='安装右键菜单')
    parser.add_argument('-u', action='store_true', help='移除右键菜单')
    parser.add_argument('-r', nargs=3, metavar=('FILE_OR_DIR', 'MARK_GROUP', 'MARK'),
                        help='改名文件或目录并指定其图标组和图标。用法: -r <文件或目录> <标签组> "<标签>"。如果标签为空字符串("")，则表示删除本组标签。')
    parser.add_argument('-timemark', nargs=2, metavar=('FILE_NAME', 'MODE'),
                        help='在文件名中添加最后修改日期的时间戳。第一个参数为 目标文件名，第二个选择时间戳 修改时间|创建时间。')
    # 解析命令行参数
    args = parser.parse_args()
    if not (args.i or args.u or args.r or args.timemark):# 如果没有提供参数，则默认执行-i功能
        args.i = True

    # 使用解析结果
    if args.i:  # -i 安装到右键菜单(供用户调用) # 需要菜单定义配置文件
        移除右键菜单() #安装前先移除
        print("正在安装右键菜单...")
        初始化配置文件名()
        安装右键菜单()
        show_message("👣提示👣", "右键菜单安装完毕")
    elif args.u: # -u 从右键菜单移除(供用户调用) # 无需任何配置文件
        print("正在移除右键菜单...")
        移除右键菜单()
        show_message("👣提示👣", "右键菜单移除完毕")
    elif args.r:# -r 按参数改名(供菜单项调用)   # 需要菜单定义配置文件 和 过滤字串配置文件
        file_or_dir, mark_group, icon = args.r[0], args.r[1], args.r[2]
        print(f"正在改名 {file_or_dir} 并设置其图标为 <{icon}> @ (组 {mark_group})...")
        初始化配置文件名()
        文件名打标签(file_or_dir, to_integer(mark_group), icon)
    elif args.timemark :# -timemark 按时间改名(供菜单项调用) # 需要菜单定义配置文件 和 过滤字串配置文件
        file_name, mode = args.timemark
        初始化配置文件名()
        文件名设置时间戳记(file_name, mode)

if __name__ == "__main__":
    main()
    sys.exit(0)

